package while_lang.tests;

import while_lang.ast.Stmt;
import while_lang.parser.ParserCup;
import while_lang.semantics.Declaration;
import while_lang.semantics.State;
import while_lang.semantics.SymbolTable;
import while_lang.semantics.Variable;
import while_lang.semantics.types.*;
import junit.framework.TestCase;

public class EvaluationTests extends TestCase {

	public void testTypes() {
	// Todos los tipos deben ser compatibles consigo mismos.
		assertTrue(IntegerType.TYPE.isCompatible(IntegerType.TYPE));
		assertTrue(NumberType.TYPE.isCompatible(NumberType.TYPE));
		assertTrue(BooleanType.TYPE.isCompatible(BooleanType.TYPE));
	// Los tipos numéricos deben ser compatibles entre sí.
		assertTrue(IntegerType.TYPE.isCompatible(NumberType.TYPE));
		assertTrue(NumberType.TYPE.isCompatible(IntegerType.TYPE));
	// El tipo booleano no debe ser compatible con ningún otro.
		assertFalse(BooleanType.TYPE.isCompatible(IntegerType.TYPE));
		assertFalse(BooleanType.TYPE.isCompatible(NumberType.TYPE));
		
	// Compatibilidades con tipos de Java.
		assertTrue(IntegerType.TYPE.isCompatible(Integer.class));
		assertTrue(IntegerType.TYPE.isCompatible(Double.class));
		assertFalse(IntegerType.TYPE.isCompatible(Boolean.class));
		assertTrue(NumberType.TYPE.isCompatible(Double.class));
		assertTrue(NumberType.TYPE.isCompatible(Integer.class));
		assertFalse(NumberType.TYPE.isCompatible(Boolean.class));
		assertTrue(BooleanType.TYPE.isCompatible(Boolean.class));
		assertFalse(BooleanType.TYPE.isCompatible(Integer.class));
		assertFalse(BooleanType.TYPE.isCompatible(Double.class));
		
	// Compatibilidades con valores.	
		assertTrue(IntegerType.TYPE.isCompatible(12));
		assertTrue(IntegerType.TYPE.isCompatible(12.3));
		assertFalse(IntegerType.TYPE.isCompatible(true));
		assertTrue(NumberType.TYPE.isCompatible(12.3));
		assertTrue(NumberType.TYPE.isCompatible(12));
		assertFalse(NumberType.TYPE.isCompatible(true));
		assertTrue(BooleanType.TYPE.isCompatible(true));
		assertFalse(BooleanType.TYPE.isCompatible(12));
		assertFalse(BooleanType.TYPE.isCompatible(12.3));
		
	// Coerciones.
		assertEquals(12, IntegerType.TYPE.coerce(12));
		assertEquals(12, IntegerType.TYPE.coerce(12.3));
		assertEquals(12.3, NumberType.TYPE.coerce(12.3));
		assertEquals(12.0, NumberType.TYPE.coerce(12));
	}
	
	public void testState() {
		State state = new State();
		state.declare(new while_lang.semantics.Variable("x", IntegerType.TYPE));
		assertEquals("x", state.variable("x").id);
		try {
			state.variable("x").value();
			fail("Obtener el valor de una variable no iniciada debería fallar.");
		} catch (Error error) {
			// Resultado esperado.
		}
		state.variable("x").assign(12);
		assertEquals(12, state.variable("x").value());
		state.variable("x").assign(77.2);
		assertEquals(77, state.variable("x").value());
		try {
			state.variable("x").assign(true);
			fail("Asignar un valor booleano a una variable entera debería fallar.");
		} catch (Error error) {
			// Resultado esperado.	
		}			
	// Redeclaración.
		try {
			state.declare(new while_lang.semantics.Variable("x", NumberType.TYPE));
			fail("Redeclarar una variable debería fallar.");
		} catch (Error error) {
			// Resultado esperado.	
		}
	}
	
	public void assertEvaluation(String code, Variable... variables) throws Exception {
		Stmt stmt = ParserCup.parseString(code);
		State state = stmt.evaluate(new State());
		for (Variable var : variables) {
			assertEquals(var.type, state.variable(var.id).type);
			assertEquals(var.value(), state.variable(var.id).value());
		}
	}
	
	public void testEvaluation() throws Exception {
		assertEvaluation("{ int x = 1; }", new Variable("x", IntegerType.TYPE, 1));
		assertEvaluation("{ int x = 1.1; }", new Variable("x", IntegerType.TYPE, 1));
		assertEvaluation("{ num x = 1; }", new Variable("x", NumberType.TYPE, 1.0));
		assertEvaluation("{ num x = 1.1; }", new Variable("x", NumberType.TYPE, 1.1));
		assertEvaluation("{ bool b = true; }", new Variable("b", BooleanType.TYPE, true));
		
		assertEvaluation("{ int x = 1; x = x + 1; }", new Variable("x", IntegerType.TYPE, 2));
		assertEvaluation("{ int x = 1; x = x + 1.1; }", new Variable("x", IntegerType.TYPE, 2));
		assertEvaluation("{ num x = 1; x = x + 1; }", new Variable("x", NumberType.TYPE, 2.0));
		assertEvaluation("{ num x = 1; x = x + 1.1; }", new Variable("x", NumberType.TYPE, 2.1));
		
		assertEvaluation("{ if true then { int x = 1; } else { } }", new Variable("x", IntegerType.TYPE, 1));
		assertEvaluation("{ if false then { } else { int x = 1; } }", new Variable("x", IntegerType.TYPE, 1));
		assertEvaluation("{ int x = 1; while false do { x = 2; } }", new Variable("x", IntegerType.TYPE, 1));
		assertEvaluation("{ int x = 1; while false do { num x = 2.1; } }", new Variable("x", IntegerType.TYPE, 1));
	}
	
	public void assertEvaluationFailure(String code) throws Exception {
		Stmt stmt = ParserCup.parseString(code);
		try {
			State state = stmt.evaluate(new State());
			fail("La evaluación de "+ code +" debería haber fallado, en lugar de dar "+ state +".");
		} catch (Error error) {
			// Resultado esperado.
		}
	}
	
	public void testEvaluationFailure() throws Exception {
		assertEvaluationFailure("{ x = 1; }");
		assertEvaluationFailure("{ int x = 1; y = 2; }");
		
		assertEvaluationFailure("{ int x = true; }");
		assertEvaluationFailure("{ bool b = 1; }");
		assertEvaluationFailure("{ bool b = 1.1; }");
		assertEvaluationFailure("{ int x = true + 1; }");
		assertEvaluationFailure("{ int x = 1 * true; }");
		assertEvaluationFailure("{ int x = true - 1; }");
		assertEvaluationFailure("{ int x = 1 / true; }");
		
		assertEvaluationFailure("{ if 1 then {} else {} }");
		assertEvaluationFailure("{ if 1.1 then {} else {} }");
		assertEvaluationFailure("{ while 1 do {} }");
		assertEvaluationFailure("{ while 1.1 do {} }");
		
		assertEvaluationFailure("{ int x = 1; int x = 2; }");
		assertEvaluationFailure("{ int x = 1; num x = 2.1; }");
		assertEvaluationFailure("{ int x = 1; bool x = true; }");
		
		assertEvaluationFailure("{ if true then { int x = 1; } else { } x = 2; }");
		assertEvaluationFailure("{ if true then { } else { num x = 1.1; } x = 2; }");
		assertEvaluationFailure("{ if true then { int x = 1; } else { num x = 1.1; } x = 2; }");
		assertEvaluationFailure("{ if true then { } else { } x = 2; }");
		assertEvaluationFailure("{ while false do { int x = 1; } x = 2; }");
		assertEvaluationFailure("{ while false do { } x = 2; }");
	}
	
	public void assertCheckTypes(String code, Declaration... declarations) throws Exception {
		Stmt stmt = ParserCup.parseString(code);
		SymbolTable table = stmt.checkTypes(new SymbolTable());
		for (Declaration decl : declarations) {
			assertEquals(decl.type, table.variable(decl.id).type);
			assertEquals(decl.hasValue, table.variable(decl.id).hasValue);
		}
	}
	
	public void testCheckTypes() throws Exception {
		assertCheckTypes("{ int x; }", new Declaration("x", IntegerType.TYPE, false));
		assertCheckTypes("{ int x = 1; }", new Declaration("x", IntegerType.TYPE, true));
		assertCheckTypes("{ num x; }", new Declaration("x", NumberType.TYPE, false));
		assertCheckTypes("{ num x = 1.1; }", new Declaration("x", NumberType.TYPE, true));
		assertCheckTypes("{ bool b; }", new Declaration("b", BooleanType.TYPE, false));
		assertCheckTypes("{ bool b = true; }", new Declaration("b", BooleanType.TYPE, true));
		
		assertCheckTypes("{ if true then { int x = 1; } else { int x = 2; } }", 
				new Declaration("x", IntegerType.TYPE, true));
		assertCheckTypes("{ if true then { num x = 1.1; } else { num x = 2.1; } }", 
				new Declaration("x", NumberType.TYPE, true));
	}
	
}
